package com.functest.orm

import com.functest.util.OrmUtil
import groovy.sql.Sql
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import java.sql.ResultSet

class FromClauseBuilder {
    SingleDbTableMapper mainMapper

    Set<SingleDbTableMapper> mappers
    SingleDbQueryBuilder queryBuilder

    FromClauseBuilder(SingleDbQueryBuilder qb) {
        this.queryBuilder = qb
        mappers = [] as Set
        mappers.addAll qb.mapper.mappers
        mainMapper = qb.mapper.mappers.first()
    }

    GString buildFromClause() {
        //assert  queryBuilder.usedMappers.size() == 1, "Can't build clause for more than one database! You should 
        // try using multiBuild() function instead of build()"

        String clause = " from ${mainMapper.fqTableName}"

        //We ALWAYS query ALL tables
        List<SingleDbTableMapper> mappers = queryBuilder.mapper.mappers

        mappers.each { mapper ->
            if (mapper != mainMapper) {
                String keyField = mapper.keyField
                MapperBinding binding = queryBuilder.mapper.getBinding(mapper)

                // bind key column

                if (!mapper.isMultiColumn(keyField)) {
                    clause = clause + " ${binding.joinType.sql} join ${mapper.fqTableName} on ("

                    boolean first = true
                    //If keyField is not defined in higher priority mappers, we simply omit binding key field - 
                    // instead conditions from binding are used only

                    if (queryBuilder.mapper.findMapper(keyField) != mapper) {
                        clause = clause + "${mapper.getFqSourceName(keyField)} = ${queryBuilder.mapper.getFqSourceName(keyField)}"
                        first = false
                    }

                    binding.conditionsList.each {
                        if (!first) {
                            clause = "${clause} and ${it.buildSql(mapper)}"
                        } else {
                            clause = "${clause} ${it.buildSql(mapper)}"
                            first = false
                        }
                    }
                    clause = clause + ')'
                } else {
                    //TODO: add support for binding multi-column keys(?)
                    throw new UnsupportedOperationException("Can't bind table by multi-column key field ${keyField} " +
                            ':(.')
                }
            }
        }
        OrmUtil.asGString(clause)
    }
}

//
//abstract class AbstractQueryBuilder implements QueryBuilder {
//
//}


@SuppressWarnings('UnnecessaryQualifiedReference')
class SingleDbQueryBuilder implements QueryBuilder {
    MultiTableMapper mapper
    Map<String, String> columnAliases = [:]
    Set<SingleDbTableMapper> usedMappers = []
    Set<String> usedFields = []
    Set<String> usedSourceFqNames = []
    boolean distinct = false
    GString columnsList = GString.EMPTY
    Closure<SingleDbTableMapper> mapperGetter = { String field = null ->
        mapper.findMapper(field)
    }


    SingleDbQueryBuilder(SingleDbTableMapper mapper, Class beanClass) {
        this.mapper = new MultiTableMapper([mapper], beanClass)
    }

    SingleDbQueryBuilder(MultiTableMapper mapper) {
        this.mapper = mapper
    }

    SingleDbQueryBuilder(List<SingleDbTableMapper> mappers, Class beanClass) {
        this.mapper = new MultiTableMapper(mappers, beanClass)
    }

    private GString getFromClause() {
        FromClauseBuilder fB = new FromClauseBuilder(this)
        fB.buildFromClause()
    }


    protected Sql getDb() {
        (mapper.mappers.first()).dbGetter()
    }

    protected void registerFieldIfNeeded(String field) {
        if (!usedFields.contains(field)) {
            usedFields.add(field)
            usedMappers.add(mapperGetter(field))
        }
    }

    protected GString getBaseClause() {
        queryBase.plus(columnsList).plus(fromClause)
    }


    GString getQueryBase() {
        distinct ? OrmUtil.asGString('select distinct ') : OrmUtil.asGString('select ')
    }

//QueryBuilder implementation
    @Override
    SingleDbQueryBuilder field(String field) {
        //TODO add support for column aliases!!!

        SingleDbTableMapper mapper = mapperGetter(field)
        registerFieldIfNeeded(field)
        List<String> fqNames = new ArrayList(mapper.getFqSourceNames(field).values())

        //Don't add field if it's already mapped!
        fqNames.removeAll(usedSourceFqNames)

        //If nothing to add - exit
        if (!fqNames) return this

        usedSourceFqNames.addAll(fqNames)

        if (!(columnsList == GString.EMPTY)) {
            columnsList = columnsList.plus(OrmUtil.asGString(' , '))
        }

//        Field f = mapper.getField(field)
//        Map fqNamez = mapper.getFqSourceNames(f.name)
//        f.getFrom().each {
//            if (usedColumns.contains(it)) {
//                columnAliases.put(it as String, "${f.name}_${f.from}" as String)
//                resColumns << fqNamez.get(it) + " as ${columnAliases.get(it)}"
//            } else {
//                resColumns << fqNamez.get(it)
//                usedColumns << it
//            }
//        }

        columnsList = columnsList.plus(OrmUtil.asGString(fqNames.join(' ,'))).plus(OrmUtil.asGString(' '))
        this
    }

    @Override
    QueryBuilder expression(String expression, alias) {
        throw new UnsupportedOperationException('Not implemented yet')
    }

    @Override
    QueryBuilder distinct() {
        distinct = true
        this
    }

    @Override
    SingleDbQueryBuilder fieldList(List<String> fields) {
        fields.each {
            registerFieldIfNeeded(it)
            field(it)
        }
        this
    }

    @Override
    SingleDbQueryBuilder allFields() {
        //TODO -> rewrite in such a way that aliases could be used

        List<String> resColumnsWithAliases = []
//        List<String> usedFields = []
        List<String> usedColumns = []

        //{
//        mapper.allFields.each { Field field ->
//            String simpleFieldName = field.simpleName
//            String fullFieldName = field.name
//            String fromName = field.from.first()
//            //HashMap.<tableName>.<columnName>
//
//            SingleDbTableMapper fieldMapper = mapper.findMapper(fullFieldName)
////            String tableName = fieldMapper.shortTableName.replace("\"", '')
//
//            String fqColumnName = fieldMapper.getFqSourceName(simpleFieldName)
//
//            if (usedFields.contains(simpleFieldName) || usedColumns.contains(fromName)) {
//                def alias = fullFieldName.replace('.','_')
//                columnAliases.put(fqColumnName, alias)
//                resColumnsWithAliases << fqColumnName + " as $alias]"
//            } else {
//                usedColumns << fromName
//                resColumnsWithAliases << fqColumnName
//                usedFields << simpleFieldName
//            }
//        }
        //}

        mapper.allFields.each { Field field ->
            Map<String, String> fqNames = mapper.getFqSourceNames(field.name)
            field.from.each { String fromColumnShortName ->
                String fullColumnName = fqNames[fromColumnShortName]
                if (usedColumns.contains(fromColumnShortName)) {
                    String alias = field.name.replace('.', '_').replace('[','').replace(']','') // Full field Name with .'s replaced
                    columnAliases.put(fullColumnName, alias)
                    resColumnsWithAliases << fullColumnName + " as ${alias}"
                } else {
                    resColumnsWithAliases << fullColumnName
                    usedColumns << fromColumnShortName
                }
            }
        }

        columnsList = columnsList.plus(resColumnsWithAliases.join(', '))
        this.usedFields = mapper.allFields.name
        usedMappers = new ArrayList(mapper.mappers)
        this
    }

    @Override
    SingleDbWhereClauseBuilder where() {
        new SingleDbWhereClauseBuilder(this)
    }

    @Override
    SingleDbWhereClauseBuilder where(Condition c) {
        new SingleDbWhereClauseBuilder(this).and(c)
    }

    @Override
    SingleDbWhereClauseBuilder where(String field, ConditionOp op, def value) {
        new SingleDbWhereClauseBuilder(this).and(new Condition(field: field, op: op, value: value))
    }

    @Override
    QueryExecutor build() {
        new SingleDbQueryExecutor(this, baseClause)
    }
}


class SingleDbWhereClauseBuilder implements WhereClauseBuilder {
    SingleDbQueryBuilder qBuilder
    List<Condition> postFilters = []
    GString whereClause = OrmUtil.asGString(' where 1=1 ')
    int bracketsCounter = 0

    SingleDbWhereClauseBuilder(SingleDbQueryBuilder builder) {
        this.qBuilder = builder
        //Add main table conditions
        List<Condition> mainTableConditions = builder.mapper.getBinding(qBuilder.mapper.mappers.first())
                .conditionsList
        mainTableConditions.each {
            and(it)
        }

    }

    @Override
    String toString(){
        "SingleDbWhereClauseBuilder ${whereClause}"
    }

    @Override
    SingleDbWhereClauseBuilder and(String field, ConditionOp op, def value) {
        qBuilder.registerFieldIfNeeded(field)
        and(new Condition(field: field, op: op, value: value))
    }



    SingleDbWhereClauseBuilder closeBrackets() {
        bracketsCounter--
        whereClause = whereClause.plus(OrmUtil.asGString(' ) '))
        this
    }

    void closeAllBrackets() {
        int mx = bracketsCounter
        for (int i = 0; i < mx; i++)
            closeBrackets()
    }



    @Override
    SingleDbWhereClauseBuilder or(Condition c) {
        if (qBuilder.mapper.isCalculated(c.field))
            throw new UnsupportedOperationException("Can't use 'or' with calculated field!")

        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(OrmUtil.asGString(' or ')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }

    @Override
    Class getBeanClass() {
        this.qBuilder.mapper.beanClass
    }

    @Override
    SingleDbWhereClauseBuilder and(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        if (!qBuilder.mapper.isCalculated(c.field)) {
            whereClause = whereClause.plus(OrmUtil.asGString(' and ')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        } else {
            postFilters << c
        }
        this
    }

    @Override
    SingleDbWhereClauseBuilder or(String field, ConditionOp op, def value) {
        qBuilder.registerFieldIfNeeded(field)
        or(new Condition(field: field, op: op, value: value))
    }

    @Override
    SingleDbWhereClauseBuilder andNot(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(OrmUtil.asGString(' and not ')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }

    @Override
    SingleDbWhereClauseBuilder orNot(Condition c) {
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(OrmUtil.asGString(' or not ')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))
        this
    }

//    SingleDbWhereClauseBuilder andOpenBrackets() {
//        bracketsCounter++
//        whereClause = whereClause.plus(asGString(' and (1=1  '))
//        this
//    }
//
//    @Override
//    WhereClauseBuilder orOpenBrackets() {
//        bracketsCounter++
//        whereClause = whereClause.plus(asGString(' or (1=1 '))
//        this
//    }

    @Override
    WhereClauseBuilder andOpenBrackets(Condition c) {
        bracketsCounter++
        if (!qBuilder.mapper.isCalculated(c.field)) {
            throw new UnsupportedOperationException("Can't use calculated field inside brackets $c")
        }
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(OrmUtil.asGString(' and (')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))

        this
    }

    @Override
    WhereClauseBuilder orOpenBrackets(Condition c) {
        bracketsCounter++
        if (!qBuilder.mapper.isCalculated(c.field)) {
            throw new UnsupportedOperationException("Can't use calculated field inside brackets $c")
        }
        qBuilder.registerFieldIfNeeded(c.field)
        whereClause = whereClause.plus(OrmUtil.asGString(' or (')).plus(c.buildSql(qBuilder.mapperGetter(c.field)))

        this
    }

    QueryExecutor build() {
        closeAllBrackets()
        GString query = qBuilder.baseClause.plus(whereClause)
        new SingleDbQueryExecutor(qBuilder, query, postFilters)
    }

}


class SingleDbQueryExecutor implements QueryExecutor {
    SingleDbQueryBuilder qBuilder
    GString query
    List<Condition> postFilters
    private final static Logger logger = LoggerFactory.getLogger(SingleDbQueryExecutor)

    @Override
    String toString() {
        "Query: $query, postFilters: ${postFilters}"
    }

    SingleDbQueryExecutor(SingleDbQueryBuilder qBuilder, GString query, List<Condition> postFilters = []) {
        this.query = query
        this.qBuilder = qBuilder
        this.postFilters = postFilters

        logger.debug("Executor created query: ${query} with additional filters ${postFilters}")

    }

    private Sql getDb() {
        qBuilder.db
    }


    private boolean matchesFilter(Map rawRow) {
        for (Condition f in postFilters) {
            Field field = qBuilder.mapper.getField(f.field)
            try {
                Object value = field.transform(field, rawRow)
                if (!f.apply(value, true)) return false
            } catch (NoSuchElementException ignored) {
                return false
            }

        }
        true
    }

    @Override
    List<Map> execute() {
        List<Map> rawRows = executeRaw()
        List rows = []

        rawRows.each { Map rawRow ->
            rows << qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
            //transformMap realizes null check for columnAliases
        }
        rows
    }

    @Override
    /**
     * Note, fields can be Aliased!!!!
     */
    List<Map> executeRaw() {
        List<Map> rawRows = []
        db.eachRow(query) { ResultSet row ->
            Map thisRow = row.toRowResult()
            if (matchesFilter(thisRow)) {
                rawRows << thisRow
            }
        }

        rawRows
    }


    @Override
    Map firstRow() {
        if (!postFilters) {
            Map rawRow = db.firstRow(query)
            if (!rawRow) return null
            if (qBuilder.columnAliases) {
                return qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
            }
            return qBuilder.mapper.transformMap(rawRow)
        }

        /* Process post-filters */

        List<Map> allRaw = executeRaw()
        for (Map rawRow : allRaw) {
            Map processedRow
            if (qBuilder.columnAliases) {
                processedRow = qBuilder.mapper.transformMap(rawRow, qBuilder.columnAliases)
            } else {
                processedRow = qBuilder.mapper.transformMap(rawRow)
            }
            if (matchesFilter(processedRow)) return processedRow
        }
        null

    }

    @Override
    Map firstRowRaw() {
        db.firstRow(query)
    }
}


